using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using ParticleEffects;


namespace ParticleEffectsCaching
{
  /// <summary>
  /// This is a game component that implements IUpdateable.
  /// </summary>
  public class ParticleEffectsCachingGame : Microsoft.Xna.Framework.Game
  {
    GraphicsDeviceManager graphics;

    public struct AnimationSettings
    {
      public Vector3 start;
      public Vector3 end;
      public float animation_time;
      public int num_frames;
      public Point frame_size;
      public Action<Vector3> add_particle;
      public Action<Vector3, GameTime, int, int> draw;
    }

    private Texture2D CacheParticleAnimations(AnimationSettings settings)
    {
      var frames_per_row = (int)Math.Sqrt(settings.num_frames) + 1;
      var frames_rt = new RenderTarget2D(GraphicsDevice, settings.frame_size.X * frames_per_row, settings.frame_size.Y * frames_per_row, 1, SurfaceFormat.Color);
      var prev_frames_rt = new RenderTarget2D(GraphicsDevice, settings.frame_size.X * frames_per_row, settings.frame_size.Y * frames_per_row, 1, SurfaceFormat.Color);
      var frame_rt = new RenderTarget2D(GraphicsDevice, settings.frame_size.X, settings.frame_size.Y, 1, SurfaceFormat.Color);
      GraphicsDevice.SetRenderTarget(0, frames_rt);
      GraphicsDevice.Clear(new Color(Vector4.Zero));
      GraphicsDevice.SetRenderTarget(0, prev_frames_rt);
      GraphicsDevice.Clear(new Color(Vector4.Zero));

      settings.add_particle(settings.start);
      for (int i = 0; i < settings.num_frames; i++)
      {
        GraphicsDevice.SetRenderTarget(0, frame_rt);
        GraphicsDevice.Clear(new Color(Vector4.Zero));
        var a = i / (settings.num_frames - 1.0f);
        var t = TimeSpan.FromSeconds(settings.animation_time * a);
        var dt = TimeSpan.FromSeconds(settings.animation_time / settings.num_frames);
        var gt = new GameTime(t, dt, t, dt);
        var p = Vector3.Lerp(settings.start, settings.end, a);

        settings.draw(p, gt, frame_rt.Width, frame_rt.Height);

        GraphicsDevice.SetRenderTarget(0, frames_rt);
        spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState);
        var rect = new Rectangle((i % frames_per_row) * settings.frame_size.X,
                                 (i / frames_per_row) * settings.frame_size.Y,
                                 settings.frame_size.X, settings.frame_size.Y);
        spriteBatch.Draw(prev_frames_rt.GetTexture(), Vector2.Zero, Color.White);
        spriteBatch.Draw(frame_rt.GetTexture(), rect, Color.White);
        spriteBatch.End();

        GraphicsDevice.SetRenderTarget(0, null);
        var tmp = prev_frames_rt;
        prev_frames_rt = frames_rt;
        frames_rt = tmp;
      }

      prev_frames_rt.Dispose();
      frame_rt.Dispose();

      return frames_rt.GetTexture();
    }

    protected override void LoadContent()
    {
      spriteBatch = new SpriteBatch(GraphicsDevice);

      base.LoadContent();
    }

    SpriteBatch spriteBatch;
    ExplosionParticleSystem explosions;
    ExplosionSmokeParticleSystem smoke;
    Random r = new Random();
    Vector3 random_vector()
    {
      Func<float> r = () => (float)(this.r.NextDouble() * 2.0 - 1.0);
      return Vector3.Normalize(new Vector3(r(), r(), r()));
    }

    Game Game { get { return this; } }

    public ParticleEffectsCachingGame()
    {
      graphics = new GraphicsDeviceManager(this);
    }

    protected override void Initialize()
    {
      var game = this;
      explosions = new ExplosionParticleSystem(game, game.Content);
      smoke = new ExplosionSmokeParticleSystem(game, game.Content);
      Game.Components.Add(explosions);
      Game.Components.Add(smoke);

      base.Initialize();
    }

    protected override void Update(GameTime gameTime)
    {
      var explosion_time = (float)Math.Max(explosions.settings.Duration.TotalSeconds, smoke.settings.Duration.TotalSeconds);
      var settings = new AnimationSettings()
      {
        start = Vector3.Zero,
        end = Vector3.Right * 5.0f * 0.0f,
        animation_time = explosion_time,
        num_frames = (int)(21 * explosion_time),
        frame_size = new Point(100, 100),
        add_particle = p =>
        {
          var num_particles_per_shot = 8;
          for (int i = 0; i < num_particles_per_shot; i++)
            explosions.AddParticle(p + 0.5f * random_vector(), 3.0f * random_vector(), 1.0f * ((float)r.NextDouble() * 0.5f + 0.5f), Color.White);
          for (int i = 0; i < num_particles_per_shot / 2; i++)
            smoke.AddParticle(p + 0.5f * random_vector(), 3.0f * random_vector(), 1.0f * ((float)r.NextDouble() * 0.5f + 0.5f), Color.Gray);
        },
        draw = (p, gt, w, h) =>
        {
          smoke.SetCamera(Matrix.CreateLookAt(Vector3.Backward * 6.0f + p, p, Vector3.Up), Matrix.CreatePerspectiveFieldOfView(1.5f, w / (float)h, 0.1f, 100.0f));
          smoke.Update(gt);
          smoke.Draw(gt);

          explosions.SetCamera(Matrix.CreateLookAt(Vector3.Backward * 6.0f + p, p, Vector3.Up), Matrix.CreatePerspectiveFieldOfView(1.5f, w / (float)h, 0.1f, 100.0f));
          explosions.Update(gt);
          explosions.Draw(gt);
        }
      };

      var explosion = CacheParticleAnimations(settings);
      explosion.Save(@"Explosion.dds", ImageFileFormat.Dds);
      Game.Exit();

      base.Update(gameTime);
    }
  }
}